/*
 * Copyright 2017-2020 The ShadowEditor Authors. All rights reserved.
 *
 * Use of this source code is governed by a MIT-style
 * license that can be found in the LICENSE file.
 * 
 * For more information, please visit: https://github.com/tengge1/ShadowEditor
 * You can also visit: https://gitee.com/tengge1/ShadowEditor
 */
import BaseEvent from './BaseEvent';
import PickVertexShader from './shader/pick_vertex.glsl';
import PickFragmentShader from './shader/pick_fragment.glsl';
import DepthVertexShader from './shader/depth_vertex.glsl';
import DepthFragmentShader from './shader/depth_fragment.glsl';
import MeshUtils from '../utils/MeshUtils';
import global from '../global';

let maxHexColor = 1;

/**
 * 使用GPU选取物体和计算鼠标世界坐标
 * @author tengge / https://github.com/tengge1
 */
class GPUPickEvent extends BaseEvent {
    constructor() {
        super();
        this.isIn = false;
        this.offsetX = 0;
        this.offsetY = 0;
        this.waitTime = 10; // 10毫秒检测一次，提升性能
        this.oldTime = 0;

        this.selectMode = 'whole';

        this.onMouseMove = this.onMouseMove.bind(this);
        this.onAfterRender = this.onAfterRender.bind(this);
        this.onResize = this.onResize.bind(this);
        this.onStorageChanged = this.onStorageChanged.bind(this);
    }

    start() {
        global.app.on(`mousemove.${this.id}`, this.onMouseMove);
        global.app.on(`afterRender.${this.id}`, this.onAfterRender);
        global.app.on(`resize.${this.id}`, this.onResize);
        global.app.on(`storageChanged.${this.id}`, this.onStorageChanged);

        this.selectMode = global.app.storage.selectMode;
    }

    stop() {
        global.app.on(`mousemove.${this.id}`, null);
        global.app.on(`afterRender.${this.id}`, null);
        global.app.on(`resize.${this.id}`, null);
        global.app.on(`storageChanged.${this.id}`, null);

        this.selectMode = 'whole';
    }

    onMouseMove(event) {
        if (event.target !== global.app.editor.renderer.domElement) { // 鼠标不在画布上
            this.isIn = false;
            global.app.call(`gpuPick`, this, {
                object: null,
                point: null,
                distance: 0
            });
            return;
        }
        this.isIn = true;
        this.offsetX = event.offsetX;
        this.offsetY = event.offsetY;
    }

    /**
     * 由于需要较高性能，所以尽量不要拆分函数。
     */
    onAfterRender() {
        if (!this.isIn || global.app.editor.gpuPickNum === 0) {
            return;
        }

        // 间隔一段时间执行一次，提高性能
        let now = new Date().getTime();
        if (now - this.oldTime < this.waitTime) {
            return;
        }
        this.oldTime = now;

        let {scene, renderer} = global.app.editor;
        const camera = global.app.editor.view === 'perspective' ? global.app.editor.camera : global.app.editor.orthCamera;

        const {width, height} = renderer.domElement;

        if (this.init === undefined) {
            this.init = true;
            this.depthMaterial = new THREE.ShaderMaterial({
                vertexShader: DepthVertexShader,
                fragmentShader: DepthFragmentShader,
                uniforms: {
                    far: {
                        value: camera.far
                    }
                }
            });

            this.renderTarget = new THREE.WebGLRenderTarget(width, height);
            this.pixel = new Uint8Array(4);

            this.nearPosition = new THREE.Vector3(); // 鼠标屏幕位置在near处的相机坐标系中的坐标
            this.farPosition = new THREE.Vector3(); // 鼠标屏幕位置在far处的相机坐标系中的坐标
            this.world = new THREE.Vector3(); // 通过插值计算世界坐标

            this.line = new THREE.Line3(this.nearPosition, this.farPosition);
            this.plane = new THREE.Plane().setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3());
        }

        // 记录旧属性
        const oldBackground = scene.background;
        const oldOverrideMaterial = scene.overrideMaterial;
        const oldRenderTarget = renderer.getRenderTarget();

        // ---------------------------- 1. 使用GPU判断选中的物体 -----------------------------------

        scene.background = null; // 有背景图，可能导致提取的颜色不准
        scene.overrideMaterial = null;
        renderer.setRenderTarget(this.renderTarget);

        // 更换选取材质
        scene.traverseVisible(n => {
            if (!(n instanceof THREE.Mesh)) {
                return;
            }
            n.oldMaterial = n.material;
            if (n.pickMaterial) {
                n.material = n.pickMaterial;
                return;
            }
            let material = new THREE.ShaderMaterial({
                vertexShader: PickVertexShader,
                fragmentShader: PickFragmentShader,
                uniforms: {
                    pickColor: {
                        value: new THREE.Color(maxHexColor)
                    }
                }
            });
            n.pickColor = maxHexColor;
            maxHexColor++;
            n.material = n.pickMaterial = material;
        });

        // 绘制并读取像素
        renderer.clear(); // 一定要清缓冲区，renderer没开启自动清空缓冲区
        renderer.render(scene, camera);
        renderer.readRenderTargetPixels(this.renderTarget, this.offsetX, height - this.offsetY, 1, 1, this.pixel);

        // 还原原来材质，并获取选中物体
        const currentColor = this.pixel[0] * 0xffff + this.pixel[1] * 0xff + this.pixel[2];

        let selected = null;

        scene.traverseVisible(n => {
            if (!(n instanceof THREE.Mesh)) {
                return;
            }
            if (n.pickMaterial && n.pickColor === currentColor) {
                selected = n;
            }
            if (n.oldMaterial) {
                n.material = n.oldMaterial;
                delete n.oldMaterial;
            }
        });

        // ------------------------- 2. 使用GPU反算世界坐标 ----------------------------------

        scene.overrideMaterial = this.depthMaterial; // 注意：this.material为undifined，写在这也不会报错，不要写错了。

        renderer.clear();
        renderer.render(scene, camera);
        renderer.readRenderTargetPixels(this.renderTarget, this.offsetX, height - this.offsetY, 1, 1, this.pixel);

        let cameraDepth = 0;

        const deviceX = this.offsetX / width * 2 - 1;
        const deviceY = -this.offsetY / height * 2 + 1;

        // TODO: nearPosition和farPosition命名反了
        this.nearPosition.set(deviceX, deviceY, 1); // 屏幕坐标系：(0, 0, 1)
        this.nearPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系：(0, 0, -far)

        this.farPosition.set(deviceX, deviceY, -1); // 屏幕坐标系：(0, 0, -1)
        this.farPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系：(0, 0, -near)

        if (this.pixel[2] !== 0 || this.pixel[1] !== 0 || this.pixel[0] !== 0) { // 鼠标位置存在物体
            let hex = (this.pixel[0] * 65535 + this.pixel[1] * 255 + this.pixel[2]) / 0xffffff;

            if (this.pixel[3] === 0) {
                hex = -hex;
            }

            cameraDepth = -hex * camera.far; // 相机坐标系中鼠标所在点的深度（注意：相机坐标系中的深度值为负值）

            const t = (cameraDepth - this.nearPosition.z) / (this.farPosition.z - this.nearPosition.z);

            this.world.set(
                this.nearPosition.x + (this.farPosition.x - this.nearPosition.x) * t,
                this.nearPosition.y + (this.farPosition.y - this.nearPosition.y) * t,
                cameraDepth
            );
            this.world.applyMatrix4(camera.matrixWorld);
        } else { // 鼠标位置不存在物体，则与y=0的平面的交点
            this.nearPosition.applyMatrix4(camera.matrixWorld); // 世界坐标系近点
            this.farPosition.applyMatrix4(camera.matrixWorld); // 世界坐标系远点
            this.line.set(this.nearPosition, this.farPosition);
            this.plane.intersectLine(this.line, this.world);
        }

        // 还原原来的属性
        scene.background = oldBackground;
        scene.overrideMaterial = oldOverrideMaterial;
        renderer.setRenderTarget(oldRenderTarget);

        // ------------------------------- 3. 输出碰撞结果 --------------------------------------------

        if (selected && this.selectMode === 'whole') { // 选择整体
            selected = MeshUtils.partToMesh(selected);
        }

        global.app.call(`gpuPick`, this, {
            object: selected, // 碰撞到的物体，没碰到为null
            point: this.world, // 碰撞点坐标，没碰到物体与y=0平面碰撞
            distance: cameraDepth // 相机到碰撞点距离
        });
    }

    onResize() {
        if (!this.renderTarget) {
            return;
        }
        const {width, height} = global.app.editor.renderer.domElement;
        this.renderTarget.setSize(width, height);
    }

    onStorageChanged(name, value) {
        if (name === 'selectMode') {
            this.selectMode = value;
        }
    }
}

export default GPUPickEvent;